/* 
 * StikiWeb Wiki - a Wiki for Hierarchical Organizations
 * $Id: PermissionManager.java,v 1.8 2006/07/20 21:11:29 biglee Exp $
 * 
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the StikiWeb Wiki.
 *
 * The Initial Developer of the Original Code is Lee Haslup (biglee@haslups.com).
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * ***** END LICENSE BLOCK ***** 
 */
package org.stikiweb.contentMagnager;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * @author Allen L (BigLee) Haslup
 *
 */
public class PermissionManager implements PageChangeListener{

	// Permission Levels for a user for a given page

	// Cannot view page
	public static final int NO_ACCESS = 0;

	// Can view page but not change or view markup
	public static final int READ = 1;

	// READ plus can enter a comment appended to 
	// bottom of page.
	public static final int COMMENT = 2;

	// COMMENT plus can view page markup
	public static final int AUDIT = 3;

	// AUDIT plus upload attachments
	public static final int ATTACH = 4;

	// ATTACH plus can edit page markup
	public static final int EDIT = 5;

	// EDIT plus can create a child page
	public static final int EXTEND = 6;

	// EXTEND plus can edit permission list and 
	// delete child pages and/or attachments.
	public static final int ADMIN = 7;

	protected HashMap userList;
	
	public class PageSpec {
		public HashMap userPermLevel = new HashMap();
		public int maxStorage=0;
		public boolean reservedStorage=false;
		public int maxStoragePerAttach=0;
		public String templateName=null;
		public String toString() {
			return "Max Storage "+maxStorage+(reservedStorage?" (reserved) ":" ")+" Max per Attachment "+maxStoragePerAttach+" Template="+templateName;
		}
	}
	
	protected TreeMap pageSpecs;

	private PermissionManager() {
	};
	private static PermissionManager solo = null;
	public static synchronized PermissionManager solo() {
		if (solo == null) {
			solo = new PermissionManager();
			solo.initialize();
		}
		return solo;
	}
	// reset -- start over
	public static void resetPM() {
		if(solo!=null) {
			solo.userList = null;
			solo.pageSpecs = null;
			solo=null;
		}
	}

	protected void initialize() {
		userList = readUserList();
		PageManager.solo().addPageChangeListener("Admin.Users",this);
		pageSpecs = readPageSpecs();
		PageManager.solo().addPageChangeListener("Admin.Pages",this);
	}

	// Note: not synchronized but caller should be.
	protected  HashMap readUserList() {
		HashMap retval = new HashMap();
		Vector usersVec = PageManager.solo().getConfigurationTable("Admin.Users");
		Iterator iUser = usersVec.iterator();
		while (iUser.hasNext()) {
			Vector userVec = (Vector)iUser.next();
			if(userVec.size()>=2) {
				String user = (String)userVec.get(0);
				String password = (String)userVec.get(1);
				retval.put(user,password);
				String roles = "";
				if(userVec.size()>2) {
					roles = (String)userVec.get(2);
					StringTokenizer st = new StringTokenizer(roles,", ");
					ArrayList roleAL = new ArrayList();
					while(st.hasMoreTokens()) {
						roleAL.add(st.nextToken());
					}
					retval.put(user+"$roles",roleAL);
				}
				StikiWebContext.mutter("User:" + user + " PW:" + password );
				StikiWebContext.mutter("Roles:" + user + " = " + roles);
			}
		}
		return retval;
	}

	// Note: not synchronized but caller should be.
	protected  TreeMap readPageSpecs() {
		TreeMap retval = new TreeMap();
		Vector pagesVec = PageManager.solo().getConfigurationTable("Admin.Pages");
		Iterator iPage =pagesVec.iterator();
		String currentPage = "";
		while (iPage.hasNext()) {
			Vector pageVec = (Vector)iPage.next();
			if(pageVec.size()>=1) {
				String field1 = (String)pageVec.get(0);
				field1=field1.trim();
				if(field1.length()>0) {
					// this entry introduces a page.  field1 is the page name.
					// the special code .. indicates the top level (the whole wiki).
					if(field1.equals("..")) field1="";
					currentPage = field1;
					PageSpec ps = (PageSpec)retval.get(currentPage);
					if(ps==null) {
						ps = new PageSpec();
						retval.put(currentPage,ps);
					}
					if(pageVec.size()>=2) {
						String field2 = (String)pageVec.get(1);
						field2 = field2.trim();
						if(field2.length()>0) try {
							ps.maxStorage = Integer.parseInt(field2);
						}
						catch (NumberFormatException e) {
							StikiWebContext.mutter(currentPage+" contains an illegal maxStorage value ("+field2+")");
						}
						if(pageVec.size()>=3){
							String field3 = (String)pageVec.get(2);
							field3=field3.trim().toUpperCase();
							ps.reservedStorage = field3.startsWith("Y") || field3.startsWith("T");
							if(pageVec.size()>=4) {
								String field4 = (String)pageVec.get(3);
								field4=field4.trim();
								if(field4.length()>0) try {
									ps.maxStoragePerAttach=Integer.parseInt(field4);
								}
								catch(NumberFormatException e) {
									StikiWebContext.mutter(currentPage+" contains an illegal maxStoragePerAttachment value ("+field2+")");
								}
								if(pageVec.size()>=5) {
									String field5 = (String)pageVec.get(4);
									field5=field5.trim();
									if(field5.length()>0) {
										ps.templateName=field5;
									}
								}
							}
							
						}
					}
					StikiWebContext.mutter("Page:" + currentPage + " Specs:" + ps.toString() );
				}
				// this entry adds a user to the current page
				else {
					if(pageVec.size()>=3) {
						String user = (String)pageVec.get(1);
						user=user.trim();
						String permLevelSt = (String)pageVec.get(2);
						permLevelSt = permLevelSt.trim();
						Integer permLevel = new Integer(permLevelFromString(permLevelSt));
						PageSpec ps = (PageSpec)retval.get(currentPage);
						if(ps==null) {
							ps = new PageSpec();
							retval.put(currentPage,ps);
						}
						ps.userPermLevel.put(user,permLevel);
						StikiWebContext.mutter("User:" + user + " Level:" + permLevel );
					}
				}
			}
		}
		return retval;
	}
	
	public synchronized void updatePageSpec(String page, PageSpec spec) {
		pageSpecs.put(page,spec);
		PageInfoNode.markAllForRecalc();
	}
	
	public synchronized void removePageSpec(String page) throws PageManagerException, IOException {
		PageSpec ps = getPageSpec(page);
		if(ps==null) return;
		getPageSpecs().remove(page);
		rewritePageSpecs();
		PageInfoNode.markAllForRecalc();
	}
	
	public synchronized void rewritePageSpecs() throws PageManagerException, IOException {
		Vector lines = new Vector();
		Iterator pSpecEntries = pageSpecs.entrySet().iterator();
		while(pSpecEntries.hasNext()) {
			Vector fields = new Vector();
			lines.add(fields);
			Map.Entry specEntry = (Map.Entry)pSpecEntries.next();
			String pageName = (String)specEntry.getKey();
			if(pageName==null || pageName.length()==0) pageName="..";
			fields.add(pageName);
			PageSpec spec = (PageSpec)specEntry.getValue();
			fields.add(Integer.toString(spec.maxStorage));
			fields.add(spec.reservedStorage?"Y":"N");
			fields.add(Integer.toString(spec.maxStoragePerAttach));
			fields.add(spec.templateName!=null?spec.templateName:"");
			HashMap userPerms = spec.userPermLevel;
			if(userPerms!=null) {
				Iterator perms = userPerms.entrySet().iterator();
				while(perms.hasNext()) {
					Vector permFields = new Vector();
					lines.add(permFields);
					Map.Entry perm = (Map.Entry) perms.next();
					permFields.add("");
					permFields.add(perm.getKey());
					permFields.add(permStringFromLevel(((Integer)perm.getValue()).intValue()));
				}
			}
		}
		PageManager.solo().saveConfigurationTable("Admin.Pages",lines);
	}
	
	public int permLevelFromString(String permSt) {
		for(int i=NO_ACCESS; i<=ADMIN; i++) {
			if(levelNames[i].equalsIgnoreCase(permSt)) {
				return i;
			}
		}
		return READ;
	}
	
	protected final static String[] levelNames = {
		"NO_ACCESS",
		"READ",
		"COMMENT",
		"AUDIT",
		"ATTACH",
		"EDIT",
		"EXTEND",
		"ADMIN"
	};
	
	public String permStringFromLevel(int level) {
		return levelNames[level];
	}
	
	public boolean checkPassword(String username, String password) {
		if(password==null) return false;
		String realPw = (String)getUserList().get(username);
		return(password.equals(realPw));
	}
	
	public TreeMap userNames() {
		TreeMap retval = new TreeMap();
		Iterator it=getUserList().keySet().iterator();
		while(it.hasNext()) {
			String name = (String)it.next();
			retval.put(name,name);
		}
		return retval;
	}
	
	public PageSpec getPageSpec(String page) {
		return (PageSpec)getPageSpecs().get(page);
	}
	
	public String getUserName(HttpServletRequest req) {
		HttpSession session = req.getSession();
		if(session==null) return "Visitor";
		String user = (String)session.getAttribute("username");
		if(user==null) return "Visitor";
		return user;
	}
	
	public String getGuestName(HttpServletRequest req) {
	HttpSession session = req.getSession();
	if(session==null) return "";
	String ident = (String)session.getAttribute("guestname");
	if(ident==null) return "";
	return ident;
	}
	
	// answer the question: is this user allowed to delete this page (or attachment)
	public boolean canDelete(String user, String page) {
		// Can't delete the Main page or the base page
		if("Main".equalsIgnoreCase(page) || "..".equalsIgnoreCase(page) || page.length()==0){
			return false;
		}
		boolean isAttachment = page.indexOf('/') >= 0;
		if(isAttachment) {
			// to delete an attachment you need ATTACH permission for the page
			return userPermLevelForPage(user,page) >= PermissionManager.ATTACH;
		}
		else {
			// to delete a page the user needs EXTEND or better on both the page itself and the parent page.
			return userPermLevelForPage(user,page) >= PermissionManager.EXTEND
			       && userPermLevelForParentPage(user,page) >= PermissionManager.EXTEND;
		}
	}
	
	public int userPermLevelForPage(String user, String page) {
		// special case for examples
		if(page.startsWith("NullPage")) {
			return NO_ACCESS;
		}
		// get the permission granted by the parent page(s).
		int parentPerm = userPermLevelForParentPage(user,page);
		// if parent permission is ADMIN then ignore any perms for this page
		// and return ADMIN
		if(parentPerm==ADMIN) return ADMIN;
		// check for permissions for this page.
		PageSpec ps = (PageSpec)pageSpecs.get(page);
		// if this page has no permissions, inherit from parent.
		if(ps==null) return parentPerm;
		// check for specific perms for this page.
		Integer perm = (Integer)ps.userPermLevel.get(user);
		// if there is no specific permission for this user and the user
		// has roles see if they are any help.
		if(perm==null) {
			ArrayList roleAL = (ArrayList)getUserList().get(user+"$roles");
			if(roleAL!=null) {
				Iterator it = roleAL.iterator();
				while(it.hasNext()){
					String role = (String)it.next();
					Integer permThisRole = (Integer)ps.userPermLevel.get(role);
					// only pick up the role permission if it is better than we already have.
					if(perm==null){
						perm = permThisRole;
					}
					else if(permThisRole!=null && permThisRole.intValue()>perm.intValue()) {
						perm = permThisRole;
					}
				}
			}
		}
		// if we still haven't found a permission -- use the default for this page.
		if(perm==null) perm=(Integer)ps.userPermLevel.get("default");
		// if there are no specific or default perms, inherit from parent.
		if(perm==null) return parentPerm;
		// return the permission granted by this page.
		return perm.intValue();
	}
	
	public int maxStorageFor(String pageName) {
		if(pageName==null) return 100000000;
		PageSpec ps = (PageSpec) pageSpecs.get(pageName);
		if(ps!=null&&ps.maxStorage>0) {
			return ps.maxStorage;
		}
		return maxStorageFor(parentOf(pageName));
	}
	
	public int maxStoragePerAttachFor(String pageName) {
		//if we run off the end of the recursion (no ancestor defines a value)
		//use 2 meg as a "reasonable" limit.
		if(pageName==null) return 20;
		//get the page spec
		PageSpec ps = (PageSpec) pageSpecs.get(pageName);
		//if the page spec defines a value use it (-1 is code for no value)
		if(ps!=null&&ps.maxStoragePerAttach>=0) {
			return ps.maxStoragePerAttach;
		}
		return maxStoragePerAttachFor(parentOf(pageName));
	}
	
	// given the name of a page, return the name of its parent, if any.
	public String parentOf(String pageName) {
		// the empty page name (the wiki base) has no parent page.
		if(pageName==null  || pageName.length()==0) {
			return null;
		}
		// parent of an attachment is the page (the part before the first slash)
		int sep = pageName.indexOf('/');
		// if page name is not an attachment name the parent is the part before the last dot
		if(sep<0) {
			sep = pageName.lastIndexOf('.');
		}
		// if there are neither dots nor slashes return the wiki base name ( "" )
		if(sep<0) {
			return "";
		}
		// parent page is the part of the name before the separator
		return pageName.substring(0,sep);
	}
	
	// given a page name return the user's permission level for its parent page.
	public int userPermLevelForParentPage(String user, String pageName) {
		// if we run off the end of the recursion and never find a permission
		// then grant EXTEND -- this is a Wiki, after all.
		if(pageName==null  || pageName.length()==0) {
			return EXTEND;
		} 
		return userPermLevelForPage(user,parentOf(pageName));
	}
	
	// get the template to use for a page
	public String templateForPage(String page, HttpServletRequest req) {
		// starting with the page, loop up through ancestor pages.
		while (true) {
			// get the page spec
			PageSpec ps = (PageSpec)pageSpecs.get(page);
			// if there is one and it defines a template, return it
			if(ps!=null&&ps.templateName!=null&&ps.templateName.length()>0) {
				return ps.templateName;			
			}
			// if we run off the end of the list
			if(page==null ||  page.length()==0) {
				return "Admin.Template";
			}
			// set to parent page
			page=parentOf(page);
		}
	}

	/* (non-Javadoc)
	 * @see org.stikiweb.contentMagnager.PageChangeListener#pageChanged(java.lang.String)
	 */
	public synchronized void pageChanged(String page) {
		if(page.equals("Admin.Users")) {
			setUserList(readUserList());
		}
		else if(page.equals("Admin.Pages")) {
			setPageSpecs(readPageSpecs());
		}
	}
	
	protected synchronized HashMap getUserList() {
		return userList;
	}
	
	// Note: not synchronized but callers should be...
	protected void setUserList(HashMap list) {
		userList = list;
	}
	
	protected synchronized TreeMap getPageSpecs() {
		return pageSpecs;
	}
	
	public String getAdminParent(String page) {
		// the wiki base has no parent.
		if(page==null  || page.length()==0) {
			return null;
		}
		String parent=parentOf(page);
		if(pageSpecs.containsKey(parent)) {
			return parent;
		}
		return getAdminParent(parent);
	}
	
	public ArrayList getAdminChildren(String page) {
		// be sure we use "" for the wiki base name
		if(page==null ) {
			page="";
		}
		// loop through the list of administered pages and build a list of 
		// all that are admin children of this page.
		Iterator it = pageSpecs.keySet().iterator();
		ArrayList retval = new ArrayList();
		while(it.hasNext()){
			String candidate = (String)it.next();
			if(page.equals(getAdminParent(candidate))) 
			{
				retval.add(candidate);
			}
		}
		return retval;
	}
	
	// Note: not synchronized but callers should be...
	protected void setPageSpecs(TreeMap specs) {
		pageSpecs = specs;
		// since the storage limits may have changed we 
		// need to recalculate any available space 
		// figures in the PageInfoNode structure.
		PageInfoNode.markAllForRecalc();
	}
	
	public static String debugDump() {
		String retval = "";
		PermissionManager inst = solo();
		TreeMap specs = inst.getPageSpecs();
		Iterator it = specs.keySet().iterator();
		retval += "Page Specs: (all storage sizes in 100k increments.)\n-----------\n";
		while(it.hasNext()) {
			String name = (String)it.next();
			String dispName = name.length() == 0 ? "(wikiBase)":name;
			PageSpec ps = (PageSpec)specs.get(name);
			retval += 
				dispName 
				+ " - storage= " + ps.maxStorage+(ps.reservedStorage?"(reserved)":"")
				+ ", attachments<="+ps.maxStoragePerAttach+ ".";
			Iterator cousinIt = ps.userPermLevel.keySet().iterator();
			if(cousinIt.hasNext()) {
				retval += " (";
				String sep="";
				while(cousinIt.hasNext()) {
					String user = (String)cousinIt.next();
					Integer perm = (Integer)ps.userPermLevel.get(user);
					String permName = inst.permStringFromLevel(perm.intValue());
					retval += sep+user+"="+permName;
					sep=",";
				}
				retval += ")";
			}
			retval += "\n";
		}
		retval += "\n";
		return retval;
	}

}